package com.geh.learn.nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

/**
 * Nio Server Test
 * <p>
 * 在介绍Selector之前先明确以下3个问题：
 * 1、selector的作用是什么？
 * 2、selector如何做到网络通信的？
 * 下面就针对上面的2个问题来展开介绍：
 * 1、Selector（选择器）是Java NIO中能够检测一到多个NIO渠道，并能够知晓渠道是否为诸如读写事件做好准备的组件。这样，一个单独的线程可以管理多个channel，从而管理多个网络连接。
 * 2、selector 操作的过程
 * 2.1 首先创建Selector
 * Selector selector = Selector.open();
 * 2.2 向selector注册channel,和感兴趣的事件，
 * channel.configureBlocking(false);
 * SelectionKey key = channel.register(selector,Selectionkey.OP_READ);
 * 注意register()方法的第二个参数， 这是一个interest集合，意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听以下4中不同类型的事件：
 * 　　服务端接收客户端连接事件   SelectionKey.OP_ACCEPT
 * 　　                   客户端连接服务端事件   SelectionKey.OP_CONNECT
 * 　　                   读事件   SelectionKey.OP_READ
 * 　　                   写事件   SelectionKey.OP_WRITE
 * 如果你对不止一种事件感兴趣，那么可以用“位或”操作符将常量连接起来如下：
 * SelectionKey key = channel.register(selector,Selectionkey.OP_READ|SelectionKey.OP_WRITE);
 * 当向Selector注册Channel时，registor()方法会返回一个SelectorKey对象。这个对象包含了一些你感兴趣的属性。
 * interest集合
 * ready集合
 * Channel
 * Selector
 * 附加的对象(可选)
 * 2.2.1 interest集合
 * 就像向Selector注册通道一节中所描述的，interest集合是你所选择的感兴趣的事件集合。可以通过SelectionKey读写interest集合，像这样:
 * int interestSet = selectionKey.interestOps();
 * boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
 * boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
 * boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
 * boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;
 * 可以看到，用“位与”操作interest 集合和给定的SelectionKey常量，可以确定某个确定的事件是否在interest集合中。
 * 2.2.2 ready集合
 * ready 集合是通道已经准备就绪的操作的集合。在一次选择(Selection)之后，你会首先访问这个ready set。Selection将在下一小节进行解释。可以这样访问ready集合：
 * int readySet = selectionKey.readyOps();
 * 可以用像检测interest集合那样的方法，来检测channel中什么事件或操作已经就绪。但是，也可以使用以下四个方法，它们都会返回一个布尔类型：
 * selectionKey.isAcceptable();
 * selectionKey.isConnectable();
 * selectionKey.isReadable();
 * selectionKey.isWritable();
 * 2.2.3 Channel + Selector
 * 从SelectionKey访问Channel和Selector很简单。如下：
 * Channel  channel  = selectionKey.channel();
 * Selector selector = selectionKey.selector();
 * 2.2.4 附加对象
 * 可以将一个对象或者更多信息附着到SelectionKey上，这样就能方便的识别某个给定的通道。例如，可以附加 与通道一起使用的Buffer，或是包含聚集数据的某个对象。使用方法如下：
 * selectionKey.attach(theObject);
 * Object attachedObj = selectionKey.attachment();
 * 还可以在用register()方法向Selector注册Channel的时候附加对象。如：
 * SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
 * 2.3 通过Selector选择通道
 * 一旦向Selector注册了一或多个通道，就可以调用几个重载的select()方法。这些方法返回你所感兴趣的事件（如连接、接受、读或写）已经准备就绪的那些通道。
 * 换句话说，如果你对“读就绪”的通道感兴趣，select()方法会返回读事件已经就绪的那些通道。
 * <p>
 * <p>
 * 下面是select()方法：（该方法是阻塞方法）
 * <p>
 * int select()
 * int select(long timeout)
 * int selectNow()
 * select()阻塞到至少有一个通道在你注册的事件上就绪了。
 * select(long timeout)和select()一样，除了最长会阻塞timeout毫秒(参数)。
 * selectNow()不会阻塞，不管什么通道就绪都立刻返回（译者注：此方法执行非阻塞的选择操作。如果自从前一次选择操作后，没有通道变成可选择的，则此方法直接返回零。）。
 * select()方法返回的int值表示有多少通道已经就绪。亦即，自上次调用select()方法后有多少通道变成就绪状态。如果调用select()方法，因为有一个通道变成就绪状态，返回了1，
 * 若再次调用select()方法，如果另一个通道就绪了，它会再次返回1。如果对第一个就绪的channel没有做任何操作，现在就有两个就绪的通道，但在每次select()方法调用之间，只有一个通道就绪了。
 * <p>
 * 2.3.1 selectedKeys()
 * 一旦调用了select()方法，并且返回值表明有一个或更多个通道就绪了，然后可以通过调用selector的selectedKeys()方法，访问“已选择键集（selected key set）”中的就绪通道。如下所示：
 * Set selectedKeys = selector.selectedKeys();
 * 当像Selector注册Channel时，Channel.register()方法会返回一个SelectionKey 对象。这个对象代表了注册到该Selector的通道。可以通过SelectionKey的selectedKeySet()方法访问这些对象。
 * 可以遍历这个已选择的键集合来访问就绪的通道。如下：
 * <p>
 * <p>
 * Set selectedKeys = selector.selectedKeys();
 * Iterator keyIterator = selectedKeys.iterator();
 * while(keyIterator.hasNext()) {
 * SelectionKey key = keyIterator.next();
 * if(key.isAcceptable()) {
 * // a connection was accepted by a ServerSocketChannel.
 * } else if (key.isConnectable()) {
 * // a connection was established with a remote server.
 * } else if (key.isReadable()) {
 * // a channel is ready for reading
 * } else if (key.isWritable()) {
 * // a channel is ready for writing
 * }
 * keyIterator.remove();
 * }
 * 这个循环遍历已选择键集中的每个键，并检测各个键所对应的通道的就绪事件。
 * <p>
 * 注意每次迭代末尾的keyIterator.remove()调用。Selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道时自己移除。下次该通道变成就绪时，Selector会再次将其放入已选择键集中。
 * <p>
 * SelectionKey.channel()方法返回的通道需要转型成你要处理的类型，如ServerSocketChannel或SocketChannel等。
 * <p>
 * wakeUp()
 * <p>
 * 某个线程调用select()方法后阻塞了，即使没有通道已经就绪，也有办法让其从select()方法返回。只要让其它线程在第一个线程调用select()方法的那个对象上调用Selector.wakeup()方法即可。阻塞在select()方法上的线程会立马返回。如果有其它线程调用了wakeup()方法，但当前没有线程阻塞在select()方法上，下个调用select()方法的线程会立即“醒来（wake up）”。
 * <p>
 * close()
 * <p>
 * <p>
 * 用完Selector后调用其close()方法会关闭该Selector，且使注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭。
 * <p>
 * 总结：1、创建Selector,
 * <p>
 * 2、创建Channel，（）
 * <p>
 * 3、向Selector中注册Channel,及感兴趣的事件，
 * <p>
 * 4.1、等待注册的事件到达，不然就一直等待 selector.select()
 * <p>
 * 4.2、获取selector中选中项的迭代器，选中的项为注册的事件 Iterator ite = this.selector.selectedKeys().iterator();
 * <p>
 * 4.3、针对选中的事件对感兴趣的事件进行处理。
 * <p>
 * 5、要想快速理解selector过程，结合代码来学习。
 * <p>
 * <p>
 * <p>
 * 2.3.2 server端实例代码
 *
 * @author Ge Hui
 */
public class NioServer {
    /**
     * 通道管理器
     */
    private Selector selector;

    public void initServer(int port) throws Exception {
        // 获得一个ServerSocket通道
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        // 设置通道为 非阻塞
        serverChannel.configureBlocking(false);
        // 将该通道对于的serverSocket绑定到port端口
        serverChannel.socket().bind(new InetSocketAddress(port));
        // 获得一耳光通道管理器
        this.selector = Selector.open();

        // 将通道管理器和该通道绑定，并为该通道注册selectionKey.OP_ACCEPT事件
        // 注册该事件后，当事件到达的时候，selector.select()会返回，
        // 如果事件没有到达selector.select()会一直阻塞

        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
    }

    /**
     * 采用轮训的方式监听selector上是否有需要处理的事件，如果有，进行处理
     *
     * @throws Exception ex
     */
    public void listen() throws Exception {
        System.out.println("start server");
        // 轮询访问selector
        while (true) {
            // 当注册事件到达时，方法返回，否则该方法会一直阻塞
            selector.select();
            // 获得selector中选中的相的迭代器，选中的相为注册的事件
            Iterator ite = this.selector.selectedKeys().iterator();
            while (ite.hasNext()) {
                SelectionKey key = (SelectionKey) ite.next();
                // 删除已选的key 以防重负处理
                ite.remove();
                // 客户端请求连接事件
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    // 获得和客户端连接的通道
                    SocketChannel channel = server.accept();
                    // 设置成非阻塞
                    channel.configureBlocking(false);
                    // 在这里可以发送消息给客户端
                    channel.write(ByteBuffer.wrap(new String("hello client").getBytes()));
                    // 在客户端 连接成功之后，为了可以接收到客户端的信息，需要给通道设置读的权限
                    channel.register(this.selector, SelectionKey.OP_READ);
                    // 获得了可读的事件

                } else if (key.isReadable()) {
                    read(key);
                }

            }
        }
    }

    /**
     * 处理 读取客户端发来的信息事件
     *
     * @param key key
     * @throws Exception ex
     */
    private void read(SelectionKey key) throws Exception {
        // 服务器可读消息，得到事件发生的socket通道
        SocketChannel channel = (SocketChannel) key.channel();
        // 穿件读取的缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(10);
        channel.read(buffer);
        byte[] data = buffer.array();
        String msg = new String(data).trim();
        System.out.println("server receive from client: " + msg);
        ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
        channel.write(outBuffer);
    }

    public static void main(String[] args) throws Throwable {
        NioServer server = new NioServer();
        server.initServer(8989);
        server.listen();
    }
}